#include <logger.h>
#include <napi_call.h>
#define NAPI_EXPERIMENTAL
#include <node_api.h>

#include <option_parser.h>

namespace {
const napi_type_tag typeTagOptionParser = {
  0x1edf75a38336451d, 0xa5ed9ce2e4c00c38
};
} // namespace

enum class OptionType {
    i32,
    u32,
    i64,
    f64,
    str,
    bol,
};

struct OptionValue {
    std::string keyname;
    OptionType type;
    union {
        int32_t i32;
        uint32_t u32;
        int64_t i64;
        double f64;
        bool bol;
    } val;
    std::string str;
};

struct OptionParserData {
    OptionParser parser;
    std::vector<struct OptionValue> vals;
    OptionParserData()
    {
        LOG2SO(DEBUG) << "Ctor";
    }

    ~OptionParserData()
    {
        LOG2SO(DEBUG) << "Dtor";
    }
};

napi_value ConsoleLog(napi_env env, const std::string &desc, napi_value val)
{
    if (val == nullptr) {
        NAPI_CALL(env, napi_throw_error(env, nullptr, "ConsoleLog got nullptr"));
    }

    napi_value globalObj = nullptr;
    NAPI_CALL(env, napi_get_global(env, &globalObj));

    napi_value consoleObj = nullptr;
    NAPI_CALL(env, napi_get_named_property(env, globalObj, "console", &consoleObj));

    napi_value logFunc = nullptr;
    NAPI_CALL(env, napi_get_named_property(env, consoleObj, "log", &logFunc));

    napi_value retVal = nullptr;
    napi_value argv[2] = {nullptr, val};
    NAPI_CALL(env, napi_create_string_utf8(env, desc.c_str(), NAPI_AUTO_LENGTH, &argv[0]));
    NAPI_CALL(env, napi_call_function(env, consoleObj, logFunc, 2, argv, &retVal));
    return retVal;
}

napi_value OptionParserCtor(napi_env env, napi_callback_info info)
{
    napi_value thiz = nullptr;
    NAPI_CALL(env, napi_get_new_target(env, info, &thiz));
    // thiz = nullptr; // trigger remove_wrap

    napi_value that = nullptr;
    size_t argc = 1;
    napi_value argv = nullptr;
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &argv, &that, nullptr));
    NAPI_PASS(ConsoleLog(env, "that:", that));

    NAPI_CALL(env, napi_type_tag_object(env, that, &typeTagOptionParser));

    auto data = new OptionParserData;
    auto finalize = [](napi_env env, void *data, void *hint) {
        if (data == nullptr) {
            return;
        }
        delete reinterpret_cast<OptionParserData *>(data);
    };
    NAPI_CALL(env, napi_wrap(env, that, data, finalize, nullptr, nullptr));

    if (ConsoleLog(env, "thiz:", thiz) == nullptr) {
        void *pdata = nullptr;
        NAPI_CALL(env, napi_remove_wrap(env, that, &pdata));
        finalize(env, data, nullptr);
        return nullptr;
    }
    return that;
}

std::tuple<void *, std::string, std::string, std::string, napi_value>
OptionParserAddOptionParse(napi_env env, napi_callback_info info)
{
    auto ret = std::make_tuple(nullptr, "", "", "", nullptr);

    napi_value thisNapiVal = nullptr;
    size_t argc = 2;
    napi_value argvNapiVal[2] = {};
    NAPI_CALL_RET(env, napi_get_cb_info(env, info, &argc, argvNapiVal, &thisNapiVal, nullptr), ret);
    if (argc < 1) {
        NAPI_CALL_RET(env, napi_throw_error(env, nullptr, "arguments need at least one string"), ret);
    }

    napi_valuetype valtype = napi_undefined;
    NAPI_CALL_RET(env, napi_typeof(env, argvNapiVal[0], &valtype), ret);
    if (valtype != napi_string) {
        NAPI_CALL_RET(env, napi_throw_type_error(env, nullptr, "1st argument need string"), ret);
    }

    char buf[32] = {};
    size_t buflen = 0;
    NAPI_CALL_RET(env, napi_get_value_string_utf8(env, argvNapiVal[0], buf, sizeof(buf), &buflen), ret);

    void *pdata = nullptr;
    NAPI_CALL_RET(env, napi_unwrap(env, thisNapiVal, &pdata), ret);

    std::string sopt = buf;
    std::string lopt = buf;
    if (lopt.size() == 1) {
        lopt = "";
    } else {
        sopt = "";
    }

    return std::make_tuple(pdata, buf, sopt, lopt, argvNapiVal[1]);
}

// add_number_option
napi_value OptionParserAddNumberOption(napi_env env, napi_callback_info info)
{
    auto [pdata, option, sopt, lopt, defaultNapiVal] = OptionParserAddOptionParse(env, info);
    if (pdata == nullptr) {
        return nullptr;
    }

    double defaultVal = {};
    if (defaultNapiVal != nullptr) {
        NAPI_CALL(env, napi_coerce_to_number(env, defaultNapiVal, &defaultNapiVal));
        NAPI_CALL(env, napi_get_value_double(env, defaultNapiVal, &defaultVal));
    }

    auto &data = *reinterpret_cast<OptionParserData *>(pdata);
    data.vals.push_back({
        .keyname = option,
        .type = OptionType::f64,
        .val = {.f64 = defaultVal},
    });
    data.parser.AddOption(sopt, lopt, data.vals.back().val.f64);
    return nullptr;
}

// add_string_option
napi_value OptionParserAddStringOption(napi_env env, napi_callback_info info)
{
    auto [pdata, option, sopt, lopt, defaultNapiVal] = OptionParserAddOptionParse(env, info);
    if (pdata == nullptr) {
        return nullptr;
    }

    std::string defaultVal = {};
    if (defaultNapiVal != nullptr) {
        char buf[32] = {};
        size_t buflen = 0;
        NAPI_CALL(env, napi_get_value_string_utf8(env, defaultNapiVal, buf, sizeof(buf), &buflen));
        defaultVal = buf;
    }

    auto &data = *reinterpret_cast<OptionParserData *>(pdata);
    data.vals.push_back({
        .keyname = option,
        .type = OptionType::str,
        .str = defaultVal,
    });
    data.parser.AddOption(sopt, lopt, data.vals.back().str);
    return nullptr;
}

// add_bool_option
napi_value OptionParserAddBoolOption(napi_env env, napi_callback_info info)
{
    auto [pdata, option, sopt, lopt, defaultNapiVal] = OptionParserAddOptionParse(env, info);
    if (pdata == nullptr) {
        return nullptr;
    }

    bool defaultVal = {};
    if (defaultNapiVal != nullptr) {
        napi_value trueNapiVal = nullptr;
        NAPI_CALL(env, napi_create_string_utf8(env, "true", NAPI_AUTO_LENGTH, &trueNapiVal));
        NAPI_CALL(env, napi_strict_equals(env, defaultNapiVal, trueNapiVal, &defaultVal));
    }

    auto &data = *reinterpret_cast<OptionParserData *>(pdata);
    data.vals.push_back({
        .keyname = option,
        .type = OptionType::bol,
        .val = {.bol = defaultVal},
    });
    data.parser.AddOption(sopt, lopt, data.vals.back().val.bol);
    return nullptr;
}

napi_value OptionParserParse(napi_env env, napi_callback_info info)
{
    napi_value thisNapiVal = nullptr;
    size_t argc = 1;
    napi_value argvNapiVal = nullptr;
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &argvNapiVal, &thisNapiVal, nullptr));
    if (argc < 1) {
        NAPI_CALL(env, napi_throw_error(env, nullptr, "arguments need a string"));
    }

    napi_valuetype valtype = napi_undefined;
    NAPI_CALL(env, napi_typeof(env, argvNapiVal, &valtype));
    if (valtype != napi_string) {
        NAPI_CALL(env, napi_throw_type_error(env, nullptr, "1st argument need string"));
    }

    // data
    void *pdata = nullptr;
    NAPI_CALL(env, napi_unwrap(env, thisNapiVal, &pdata));
    auto &data = *reinterpret_cast<OptionParserData *>(pdata);

    napi_value splitFuncNapiVal = nullptr;
    NAPI_CALL(env, napi_get_named_property(env, argvNapiVal, "split", &splitFuncNapiVal));

    napi_value argsNapiVal = nullptr;
    napi_value splitArgNapiVal = nullptr;
    NAPI_CALL(env, napi_create_string_utf8(env, " ", NAPI_AUTO_LENGTH, &splitArgNapiVal));
    NAPI_CALL(env, napi_call_function(env, argvNapiVal, splitFuncNapiVal, 1, &splitArgNapiVal, &argsNapiVal));
    ConsoleLog(env, "args:", argsNapiVal);

    uint32_t argslen = 0;
    napi_get_array_length(env, argsNapiVal, &argslen);
    std::vector<std::string> strs;
    strs.reserve(argslen);
    for (uint32_t i = 0; i < argslen; i++) {
        napi_value element;
        napi_get_element(env, argsNapiVal, i, &element);

        char buf[32] = {};
        size_t buflen = 0;
        napi_get_value_string_utf8(env, element, buf, sizeof(buf), &buflen);

        strs.push_back(buf);
    }

    std::vector<const char *> parsingArgv;
    parsingArgv.reserve(strs.size());
    for (const auto &str : strs) {
        parsingArgv.push_back(str.c_str());
    }

    if (data.parser.Parse(parsingArgv.size(), parsingArgv.data())) {
        NAPI_CALL(env, napi_throw_error(env, nullptr, data.parser.GetErrorString().c_str()));
    }

    napi_value retObjNapiVal = nullptr;
    NAPI_CALL(env, napi_create_object(env, &retObjNapiVal));

    for (const auto &val : data.vals) {
        napi_handle_scope scope = nullptr;
        napi_open_handle_scope(env, &scope);
        napi_value elementNapiVal = nullptr;
        switch (val.type) {
            case OptionType::i32: napi_create_int32(env, val.val.i32, &elementNapiVal); break;
            case OptionType::u32: napi_create_uint32(env, val.val.u32, &elementNapiVal); break;
            case OptionType::i64: napi_create_int64(env, val.val.i64, &elementNapiVal); break;
            case OptionType::f64: napi_create_double(env, val.val.f64, &elementNapiVal); break;
            case OptionType::str:
                  napi_create_string_utf8(env, val.str.c_str(), NAPI_AUTO_LENGTH, &elementNapiVal);
                  break;
            case OptionType::bol: napi_get_boolean(env, val.val.bol, &elementNapiVal); break;
        }
        NAPI_CALL(env, napi_set_named_property(env, retObjNapiVal, val.keyname.c_str(), elementNapiVal));
        napi_close_handle_scope(env, scope);
    }

    napi_value skippedArgsNapiVal = nullptr;
    napi_create_array_with_length(env, data.parser.GetSkippedArgc(), &skippedArgsNapiVal);
    for (int32_t i = 0; i < data.parser.GetSkippedArgc(); i++) {
        napi_handle_scope scope = nullptr;
        napi_open_handle_scope(env, &scope);
        napi_value argvNapiVal = nullptr;
        napi_create_string_utf8(env, data.parser.GetSkippedArgv()[i], NAPI_AUTO_LENGTH, &argvNapiVal);
        napi_set_element(env, skippedArgsNapiVal, i, argvNapiVal);
        napi_close_handle_scope(env, scope);
    }

    napi_set_named_property(env, retObjNapiVal, "args", skippedArgsNapiVal);
    return retObjNapiVal;
}

struct FinalizerData {
    std::vector<int> ints;
    napi_ref finalizerRef = nullptr;
};

napi_value Finalizer(napi_env env, napi_callback_info info)
{
    size_t argc = 2;
    napi_value argvNapiVals[2] = {};
    void *pdata = nullptr;
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argvNapiVals, nullptr, &pdata));
    if (pdata == nullptr) {
        NAPI_CALL(env, napi_throw_error(env, nullptr, "data is nullptr"));
        return nullptr;
    }

    const auto &operationNapiVal = argvNapiVals[0];

    napi_valuetype valtype = napi_undefined;
    NAPI_CALL(env, napi_typeof(env, operationNapiVal, &valtype));
    if (valtype != napi_string) {
        NAPI_CALL(env, napi_invalid_arg);
    }

    char buf[32] = {};
    size_t buflen = 0;
    NAPI_CALL(env, napi_get_value_string_utf8(env, operationNapiVal, buf, sizeof(buf), &buflen));

    auto &data = *reinterpret_cast<struct FinalizerData *>(pdata);

    std::string operation = buf;
    if (operation == "add") {
        const auto &valueNapiVal = argvNapiVals[1];
        napi_value numberVal = nullptr;
        NAPI_CALL(env, napi_coerce_to_number(env, valueNapiVal, &numberVal));
        int i32 = 0;
        NAPI_CALL(env, napi_get_value_int32(env, numberVal, &i32));
        data.ints.push_back(i32);
    }

    if (operation == "get") {
        napi_value arrayNapiVal = nullptr;
        NAPI_CALL(env, napi_create_array(env, &arrayNapiVal));
        for (int i = 0; i < data.ints.size(); i++) {
            napi_handle_scope scope = nullptr;
            napi_open_handle_scope(env, &scope);
            napi_value int32NapiVal = nullptr;
            NAPI_CALL(env, napi_create_int32(env, data.ints[i], &int32NapiVal));
            NAPI_CALL(env, napi_set_element(env, arrayNapiVal, i, int32NapiVal));
            napi_close_handle_scope(env, scope);
        }
        return arrayNapiVal;
    }
    return nullptr;
}

void FinalizeFinalizerData(napi_env env, void *data, void *hint)
{
    LOG2SO(DEBUG) << "FinalizeFinalizerData";
    if (data == nullptr) {
        return;
    }
    delete reinterpret_cast<struct FinalizerData *>(data);
}

napi_value IsOptionParser(napi_env env, napi_callback_info info)
{
    napi_value isOptionParserNapiVal = nullptr;

    size_t argc = 1;
    napi_value argvNapiVal = nullptr;
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &argvNapiVal, nullptr, nullptr));

    bool isOptionParser = false;
    if (argc >= 1) {
        NAPI_CALL(env, napi_check_object_type_tag(env, argvNapiVal, &typeTagOptionParser, &isOptionParser));
    }

    NAPI_CALL(env, napi_get_boolean(env, isOptionParser, &isOptionParserNapiVal));
    return isOptionParserNapiVal;
}

/* napi_value */
NAPI_MODULE_INIT(/* napi_env env, napi_value exports */)
{
    struct FinalizerData *finalizerData = new struct FinalizerData;
    napi_property_descriptor classProps[] = {
        {.utf8name = "add_number_option", .method = OptionParserAddNumberOption},
        {.utf8name = "add_string_option", .method = OptionParserAddStringOption},
        {.utf8name = "add_bool_option", .method = OptionParserAddBoolOption},
        {.utf8name = "parse", .method = OptionParserParse},
        {.utf8name = "finalizer", .method = Finalizer, .data = finalizerData},
    };
    for (auto &prop : classProps) {
        constexpr auto methodAttrs = napi_writable | napi_enumerable | napi_configurable;
        prop.attributes = static_cast<napi_property_attributes>(methodAttrs);
    }
    napi_value classVal = nullptr;
    NAPI_CALL(env, napi_define_class(env, "OptionParser", NAPI_AUTO_LENGTH,
                                     OptionParserCtor, nullptr,
                                     sizeof(classProps) / sizeof(*classProps), classProps,
                                     &classVal));
    NAPI_CALL(env, napi_set_named_property(env, exports, "option_parser", classVal));
    NAPI_CALL(env, napi_add_finalizer(env,
        classVal, finalizerData, FinalizeFinalizerData, nullptr, &finalizerData->finalizerRef));

    napi_value isOptionParserFuncNapiVal = nullptr;
    NAPI_CALL(env, napi_create_function(env,
                                        "is_option_parser",
                                        NAPI_AUTO_LENGTH,
                                        IsOptionParser,
                                        nullptr,
                                        &isOptionParserFuncNapiVal));
    NAPI_CALL(env, napi_set_named_property(env, exports, "is_option_parser", isOptionParserFuncNapiVal));
    return exports;
}
